/*
 * 
 * Copyright (C) 2010 Wilfried Elmenreich, Anita Sobe 
 * This file is part of VideoNetwork.
 * 
 * VideoNetwork is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 
 * You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>. 
 */
package consumer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import proxy.ServerNode;
import proxy.UnitContainer;
import proxy.VideoUnit;
import simulation.Simulator;

/**
 * The Consumer class stores references to the videos currently viewed and
 * calculates the next update instant (when a video is over)
  *
 */
public class Consumer {

	//
	private static Random rand;
	// consumer id
	private int id;
	// increment in overall simulation

	private static int nextid = 0;
	/**
	 * pMindChange is the probability that a consumer adjusts her taste when
	 * viewing a video
	 */
	private static double pMindChange = 0.1;
	/**
	 * 
	 */
	private static int noViewings = 4;
	/**
	 * 
	 */
	private static int noFavorites = 3 * noViewings;

	/**
	 * 
	 */
	private ArrayList<VideoUnit> favoriteList = new ArrayList<VideoUnit>();

	/**
	 * sum of distances in favorite list, used to determine next preference
	 * based on cumulative probability
	 */
	private double sumDistances = 0.0;
	private double interestRadius = 0.0;
	//
	private int[] likesContent;

	// history
	private ArrayList<Viewing> viewings = new ArrayList<Viewing>();
	// current requests+viewing
	private ArrayList<Viewing> currentlyDemanding = new ArrayList<Viewing>();
	// only one unit at a time
	private Viewing currentlyViewing = null;

	private Simulator simulator;
	private ServerNode server;

	/**
	 * 
	 * @param simulator
	 * @param server
	 */
	public Consumer(Simulator simulator, ServerNode server) {
		id = nextid++;
		this.simulator = simulator;
		this.server = server;
		initLikesContent();
	}

	/**
	 * 
	 */
	private void initLikesContent() {
		likesContent = new int[simulator.getContentTypes()];
		for (int i = 0; i < simulator.getContentTypes(); i++)
			likesContent[i] = rand.nextInt(100);
	}

	/**
	 * A request consists of a sequence of units. All units are requested at the
	 * same time, but with different priority. One unit at a time is presented
	 * to the user
	 */
	public void processSequence() {

		// check if there is something to present
		if (currentlyViewing == null) {

			if (currentlyDemanding.size() == 0) {
				// maybe new taste?
				if (rand.nextDouble() < pMindChange) {
					chooseFavorites(simulator.getVideoUnitDirectory());
				}
				generateNewRequest(true);
				server.increaseInterest(currentlyDemanding, true);
			} else {
				// there is a request that has to be fulfilled
				// check if the unit is local and can be started
				Viewing next = currentlyDemanding.get(0);

				UnitContainer uc = server.searchPresentUnit(next.getVideoUnit()
						.getId());
				if (uc != null) {
					// is local, but not started yet
					presentSequentialLocalUnit(next, uc);
				}
				server.increaseInterest(currentlyDemanding, false);
			}
		} else {
			// check if unit is done, delete from list
			if (simulator.getSimTime() >= currentlyViewing.getViewEndTime()) {
				handleUnitPresentationDone();
			} else {
				// currently running, increase for other units
				server.increaseInterest(currentlyDemanding, false);
			}
		}
	}

	/**
	 * A request consists of a number of units. If one of the units is
	 * presented, new requests for units are generated. Presentation starts
	 * after each unit is available - i.e. parallel presentation is possible
	 */
	public void process() {

		// 1) fill up viewings
		generateNewRequest(false);

		// 2) present local units to client and request others
		ArrayList<Viewing> stillToDeliver = new ArrayList<Viewing>();
		for (Viewing vi : viewings) {
			if (simulator.getSimTime() < vi.getViewStartTime()) {

				UnitContainer uc = server.searchPresentUnit(vi.getVideoUnit()
						.getId());
				if (uc != null) {
					// it is present, immediately start viewing
					vi.setViewStartTime(simulator.getSimTime());
					vi.setViewEndTime(simulator.getSimTime()
							+ vi.getVideoUnit().playLength());
					uc.viewedUntil = vi.getViewEndTime();
					simulator.delayStat.add(vi.getViewStartTime()
							- vi.getRequestTime());
					simulator.log("viewing u" + uc.getVideoUnit().getId()
							+ " for " + vi.getVideoUnit().playLength());
					break;
				} else {
					stillToDeliver.add(vi);
				}
			}
			// 3) cleanup presentations that are done
			else if (simulator.getSimTime() > vi.getViewEndTime()) {
				// possibly the user now adjusts her taste to what she has
				// just
				// seen
				if (rand.nextDouble() < pMindChange) {
					for (int i = 0; i < likesContent.length; i++)
						likesContent[i] = vi.getVideoUnit().content[i];
					// favorite videos must be recalculated for user's new
					// taste
					chooseFavorites(simulator.getVideoUnitDirectory());
				}
				viewings.remove(viewings.indexOf(vi));
			}
		}
		// all units that have still to be delivered
		server.increaseInterest(stillToDeliver, false);

	}

	/**
	 * chooseFavorites traverses through the list of all videos and fills the
	 * favoriteList with the videoUnit objects which are most interesting for
	 * this user. It also calculates sumDistances which is used to determine the
	 * probabilities of a unit being chosen.
	 * 
	 * @param videoUnits
	 */
	public void chooseFavorites(ArrayList<VideoUnit> videoUnits) {
		// empty favorites list
		favoriteList = new ArrayList<VideoUnit>();
		sumDistances = 0.0;

		List<VideoUnit> topFavorites = new LinkedList<VideoUnit>();

		for (int i = 0; i < videoUnits.size(); i++) {
			VideoUnit vu = videoUnits.get(i);
			vu.calculateDistance(likesContent);

			// if we have enough we can pick
			if (i > noFavorites) {
				if (vu.getDistance() > topFavorites.get(0).getDistance()) {
					continue;
				}
			}
			topFavorites.add(vu);
			Collections.sort(topFavorites);

		}
		System.out.println("print favorites");
		for (VideoUnit v : topFavorites) {
			System.out.println(v.getId() + " " + v.getDistance());
		}
		interestRadius = topFavorites.get(0).getDistance();
		for (int i = 0; i < topFavorites.size(); i++) {
			favoriteList.add(topFavorites.get(i));
			// probability to view a video is based on following formula
			sumDistances += interestRadius - topFavorites.get(i).getDistance();
		}
	}

	private void generateNewRequest(boolean sequential) {

		// model different number of viewings (request)
		int localNoViewings = rand.nextInt(noViewings) + 1;
		int size;
		if (sequential) {
			size = currentlyDemanding.size();
		} else {
			size = viewings.size();
		}
		while (size < localNoViewings) {

			double p = rand.nextDouble() * sumDistances;
			double cumulation = 0.0;

			VideoUnit vu = null;

			for (int i = 0; i < favoriteList.size(); i++) {
				vu = favoriteList.get(i);
				cumulation += interestRadius - vu.getDistance();
				if (cumulation > p) {
					if (sequential) {
						if (currentlyDemanding.contains(vu))
							continue;

						if (currentlyDemanding.size() >= localNoViewings) {
							break;
						}
					} else {
						if (viewings.contains(vu))
							continue;
						if (viewings.size() >= localNoViewings) {
							break;
						}
					}
					Viewing vi = new Viewing(vu, simulator.getSimTime());
					if (sequential) {
						currentlyDemanding.add(vi);
					}
					viewings.add(vi);
				}
			}
			if (sequential) {
				size = currentlyDemanding.size();
			} else {
				size = viewings.size();
			}
		}
	}

	private void presentSequentialLocalUnit(Viewing next, UnitContainer uc) {
		next.setViewStartTime(simulator.getSimTime());
		next.setViewEndTime(simulator.getSimTime()
				+ next.getVideoUnit().playLength());
		uc.viewedUntil = next.getViewEndTime();
		simulator.delayStat
				.add(next.getViewStartTime() - next.getRequestTime());
		simulator.hitStat.add(next.getViewStartTime() - next.getRequestTime());
		simulator.log("c:" + id + " at S:" + server.getId() + " viewing u"
				+ uc.getVideoUnit().getId() + " for "
				+ next.getVideoUnit().playLength() + " startup delay: "
				+ (next.getViewStartTime() - next.getRequestTime()));
		currentlyViewing = next;
	}

	private void handleUnitPresentationDone() {

		currentlyDemanding.remove(0);
		currentlyViewing = null;
		// delayHack! all units are requested at the same time, but
		// start to be shown after unit before is finished!
		if (currentlyDemanding.size() > 0) {
			Viewing next = currentlyDemanding.get(0);
			next.setRequestTime(simulator.getSimTime());
			// try to present next unit
			UnitContainer uc = server.searchPresentUnit(next.getVideoUnit()
					.getId());
			if (uc != null) {
				presentSequentialLocalUnit(next, uc);
			} else {
				server.increaseInterest(currentlyDemanding, false);
			}
		} else {
			// request done
			simulator.seqRequestFulfilledCounter++;
		}
	}

	public static void setNextId(int i) {
		nextid = i;
	}

	public int getId() {
		return id;
	}

	public ServerNode getServer() {
		return server;
	}

	public ArrayList<Viewing> getViewings() {
		return viewings;
	}

	public ArrayList<VideoUnit> getFavoriteList() {
		return favoriteList;
	}

	public static void setRand(Random rand2) {
		rand = rand2;
	}

}
